﻿/**
 * DynaShape
 *
 * @author		Allen Chou
 * @version		1.2.1 (last update: Dec 3 2008)
 * @link        http://cjcat.blogspot.com
 * @link		http://cjcat2266.deviantart.com
 */

 /**
  * The curve drawing functionality is based on works by Andy Woodruff (http://cartogrammar.com/blog)
  */
 
package idv.cjcat.display.dynashape {
	import flash.display.*;
	import flash.events.*;
	import flash.geom.*;
	
	import idv.cjcat.*;
	import idv.cjcat.display.*;
	
	/**
	 * A <code>DynaShape</code> object executes commands stored in <code>DynaGraphics</code> objects it contains.
	 * <p>
	 * You can regard a <code>DynaGraphics</code> object as a series of stored drawing commands.
	 * Upon each call of <code>DynaShape.render()</code>,
	 * these drawing commands are executed.
	 * </p>
	 * <p>
	 * What this rendering is different from the original Flash drawing API is that,
	 * a <code>DynaGraphics</code> object only holds references to target objects;
	 * not until a render call would the coordinate values of these target objects be fectched.
	 * So if these objects' coordinates are changed through time,
	 * each render call would redraw the graphics according to these new coordinates.
	 * </p>
	 * 
	 * @example
	 * <listing>
	 * //This example creates a morphing star shape with black line and orange fill.
	 * 
	 * import flash.events.Event;
	 * import flash.utils.getTimer;
	 * import idv.cjcat.display.LineStyle;
	 * import idv.cjcat.display.Fill;
	 * import idv.cjcat.display.dynashape.DynaShape;
	 * import idv.cjcat.display.dynashape.DynaGraphics;
	 * import idv.cjcat.geom.Vector2;
	 * 
	 * //create dynamic shape
	 * var ds:DynaShape = new DynaShape();
	 * ds.x = ds.y = 200;
	 * var vectors:Array = new Array();
	 * var vec:Vector2=new Vector2(0,100);
	 * var i:int;
	 * for (i = 0; i &lt; 10; i++) {
	 * 	vectors.push(new Vector2());
	 * }
	 * 
	 * //connects back to the first point
	 * vectors.push(vectors[0]);
	 * for (i = 0; i &lt; vectors.length; i++) {
	 * 	vectors[i] = vec.rotate(2 &#42; Math.PI / (vectors.length - 1) &#42; i);
	 * }
	 * 
	 * //define styles
	 * var blackLine:LineStyle=new LineStyle(3,0x000000);
	 * var orangeFill:Fill=new Fill(0xFFcc00, 1);
	 * 
	 * //create DynaGraphics
	 * var star:DynaGraphics = new DynaGraphics();
	 * 
	 * //set line style and fill
	 * star.lineStyle(blackLine);
	 * star.beginFill(orangeFill);
	 * 
	 * //line through and end fill
	 * star.moveTo(vectors[0]);
	 * star.lineThrough(vectors);
	 * star.endFill();
	 * 
	 * //add DynaGraphics
	 * ds.add(star);
	 * addChild(ds);
	 * 
	 * //main loop
	 * addEventListener(Event.ENTER_FRAME, update);
	 * function update(e:Event):void {
	 * 	for (i = 0; i &lt; vectors.length; i++) {
	 * 		if (i%2==0) {
	 * 			vectors[i].length=100 + 60 &#42; Math.cos(getTimer() / 200);
	 * 		} else {
	 * 			vectors[i].length=100 + 60 &#42; Math.sin(getTimer() / 200);
	 * 		}
	 * 	}
	 * 
	 * 	//render shape
	 * 	ds.render();
	 * }</listing>
	 */
	public class DynaShape extends Sprite implements IRenderable {
		
		private var _graphics:Vector.<DynaGraphics> = new Vector.<DynaGraphics>();
		
		/**
		 * Creates a dynamic shape.
		 * @param	initGraphics  A vector of initial paths.
		 */
		public function DynaShape(initGraphics:Vector.<DynaGraphics> = null) { if (initGraphics != null) for (var i:uint = 0; i < initGraphics.length; i++) add(initGraphics[i]); }
		
		/**
		 * Addes a <code>DynaGraphics</code> object to the dynamic shape.
		 * @param	dynaGraphics  The <code>DynaGraphics</code> object.
		 */
		public function add(dynaGraphics:DynaGraphics):void {
			_graphics.push(dynaGraphics);
		}
		
		/**
		 * Removes a <code>DynaGraphics</code> object from the dynamic shape.
		 * @param	dynaGraphics  The <code>DynaGraphics</code> object.
		 */
		public function remove(dynaGraphics:DynaGraphics):void {
			for (var i:uint = _graphics.length - 1; i >= 0; i--) {
				if (_graphics[i] === dynaGraphics) _graphics.splice(i, 1);
			}
		}
		
		/**
		 * Clears all graphics.
		 */
		public function clear():void { _graphics = new Vector.<DynaGraphics>(); }
		
		
		private var p:uint, q:uint, r:uint;
		private var _commands:Vector.<int>;
		private var _data:Vector.<Number>;
		private var _lineStyle:LineStyle;
		private var _fill:Fill;
		private var index:uint;
		
		private var _obj:Object;
		private var _objs:Vector.<Object>;
		private var _vertices:Vector.<Number>;
		private var _indices:Vector.<int>;
		private var _uvtData:Vector.<Number>;
		
		/**
		 * Renders the dynamic shape. This method can be passed as an event listener.
		 * <p>This method can be used as an event listener.</p>
		 */
		public function render(e:Event = null):void {
			graphics.clear();
			for (p = 0; p < _graphics.length; p++) {
				if (!_graphics[p].active) continue;
				index = 0;
				_objs = _graphics[p]._preData;
				_commands = new Vector.<int>();
				_data = new Vector.<Number>();
				for (q = 0; q < _graphics[p]._preCommands.length; q++) {
					switch (_graphics[p]._preCommands[q]) {
						case DynaGraphicsCommand.MOVE_TO:
							_commands.push(1);
							_data.push(_objs[index].x, _objs[index].y);
							index++;
							break;
							
						case DynaGraphicsCommand.LINE_TO:
							_commands.push(2);
							_data.push(_objs[index].x, _objs[index].y);
							index++;
							break;
							
						case DynaGraphicsCommand.CURVE_TO:
							_commands.push(3);
							_data.push(_objs[index].x, _objs[index].y, _objs[index + 1].x, _objs[index + 1].y);
							index += 2;
							break;
							
						case DynaGraphicsCommand.LINE_THROUGH:
							for (r = 0; r < (_objs[index] as Array).length; r++) {
								_commands.push(2);
								_data.push((_objs[index] as Array)[r].x, (_objs[index] as Array)[r].y);
							}
							index++;
							break;
							
						case DynaGraphicsCommand.CURVE_THROUGH:
							processCurve(_objs[index] as CurveThroughData);
							index++;
							break;
							
						case DynaGraphicsCommand.LINE_STYLE:
							graphics.drawPath(_commands, _data, _graphics[p].winding);
							_commands = new Vector.<int>();
							_data = new Vector.<Number>();
							
							if (LineStyle(_objs[index]).alpha != 0) CJDisplay.lineStyle(this, _objs[index] as LineStyle);
							index++;
							break;
							
						case DynaGraphicsCommand.BEGIN_FILL:
							graphics.drawPath(_commands, _data, _graphics[p].winding);
							_commands = new Vector.<int>();
							_data = new Vector.<Number>();
							
							if (Fill(_objs[index]).alpha != 0) CJDisplay.beginFill(this, _objs[index] as Fill);
							index++;
							break;
							
						case DynaGraphicsCommand.END_FILL:
							graphics.drawPath(_commands, _data, _graphics[p].winding);
							_commands = new Vector.<int>();
							_data = new Vector.<Number>();
							
							graphics.endFill();
							break;
							
						case DynaGraphicsCommand.DRAW_TRIANGLES:
							graphics.drawPath(_commands, _data, _graphics[p].winding);
							_commands = new Vector.<int>();
							_data = new Vector.<Number>();
							
							_vertices = new Vector.<Number>();
							for each (_obj in _objs[index]) _vertices.push(_obj.x, _obj.y);
							if (_objs[index + 1] != null) {
								_indices = new Vector.<int>();
								for each (var i:int in _objs[index + 1]) _indices.push(i);
							} else {
								_indices = null;
							}
							if (_objs[index + 2] != null) {
								_uvtData = new Vector.<Number>();
								for each (_obj in _objs[index + 2]) _uvtData.push(_obj.x, _obj.y);
							} else {
								_uvtData = null;
							}
							index += 3;
							
							graphics.drawTriangles(_vertices, _indices, _uvtData);
							break;
							
						case DynaGraphicsCommand.BEGIN_BITMAP_FILL:
							graphics.drawPath(_commands, _data, _graphics[p].winding);
							_commands = new Vector.<int>();
							_data = new Vector.<Number>();
							
							graphics.beginBitmapFill(_objs[index] as BitmapData);
							index++;
							break;
					}
				}
				graphics.drawPath(_commands, _data, _graphics[p].winding);
			}
		}
		
		//here the curve-through command is processed.
		private var first:int;
		private var last:int;
		private var pts:Array;
		private var ctrlPts:Vector.<Vector.<Object>>;
		private var p0:Object, p1:Object, p2:Object, d0:Number, d1:Number, d2:Number;
		private var pt0:Point, pt1:Point, pt2:Point;
		private var rx:Number, ry:Number, theta:Number;
		private var ctrlDist:Number, ctrlScaleFactor:Number, ctrlAngle:Number, ctrlPt1:Point, ctrlPt2:Point
		private var bz:BezierSegment;
		private var i:int, t:Number, v:Point, k:Number, l:Number, sign:Number;
		private function processCurve(ctd:CurveThroughData):void {
			pts = ctd.objs;
			first = 1;
			last = pts.length - 1;
			
			//check if the curve is closed
			if (pts[0].x == pts[pts.length-1].x && pts[0].y == pts[pts.length-1].y){
				first = 0;
				last = pts.length;
			}
			
			//control poitns
			ctrlPts = new Vector.<Vector.<Object>>(last - first + 1);
			for (i = first; i < last; i++) {
				
				p0 = (i - 1 < 0) ? (pts[pts.length - 2]) : (pts[i - 1]);	// If the first point (of a closed line), use the second-to-last point as the previous point
				p1 = pts[i];
				p2 = (i + 1 == pts.length) ? (pts[1]) : (pts[i + 1]);	
				
				d0 = Math.sqrt(Math.pow(p0.x - p1.x, 2) + Math.pow(p0.y - p1.y, 2)); //Point.distance(p0, p1);
				if (d0 < 0.001) d0 = 0.001;
				d1 = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); //Point.distance(p1, p2);
				if (d1 < 0.001) d1 = 0.001;
				d2 = Math.sqrt(Math.pow(p0.x - p2.x, 2) + Math.pow(p0.y - p2.y, 2)); //Point.distance(p0, p2);
				if (d2 < 0.001) d2 = 0.001;
				
				pt0 = new Point(p0.x-p1.x,p0.y-p1.y);
				pt1 = new Point(p1.x,p1.y);
				pt2 = new Point(p2.x-p1.x,p2.y-p1.y);
				
				if (d0 > d1) pt0.normalize(d1);
				else if (d1 > d0) pt2.normalize(d0);
				
				pt0.offset(p1.x, p1.y);
				pt2.offset(p1.x, p1.y);
				
				rx = (pt1.x - pt0.x) + (pt1.x - pt2.x);	// sum of x components
				ry = (pt1.y - pt0.y) + (pt1.y - pt2.y);
				theta = Math.atan2(ry, rx);	// angle of the new vector
				
				k = d1 * d1 + d0 * d0 - d2 * d2;
				ctrlDist = Math.min(d0, d1) * 0.5;
				l = Math.pow(2 * d1 * d0, 2) - k * k;
				sign = (l >= 0) ? (1) : ( -1);
				
				ctrlScaleFactor = Math.atan2(sign * Math.sqrt(sign * l), k) / Math.PI;
				ctrlDist *= ((1 - ctd.sharpness) + ctd.sharpness * ctrlScaleFactor);
				ctrlAngle = theta + Math.PI / 2;
				
				ctrlPt1 = Point.polar(ctrlDist, ctrlAngle);
				ctrlPt2 = Point.polar(ctrlDist, ctrlAngle + Math.PI);
				
				ctrlPt1.offset(p1.x, p1.y);
				ctrlPt2.offset(p1.x, p1.y);
				
				if (Point.distance(ctrlPt2, pt2) > Point.distance(ctrlPt1 , pt2)){
					ctrlPts[i] = new Vector.<Object>();
					ctrlPts[i].push(ctrlPt2);
					ctrlPts[i].push(ctrlPt1);
				} else {
					ctrlPts[i] = new Vector.<Object>();
					ctrlPts[i].push(ctrlPt1);
					ctrlPts[i].push(ctrlPt2);
				}
			}
			
			// If this isn't a closed line
			if (first == 1){
				// Draw a regular quadratic Bézier curve from the first to second points, using the first control point of the second point
				_commands.push(3);
				_data.push(ctrlPts[1][0].x, ctrlPts[1][0].y, pts[1].x, pts[1].y);
			}
			// Loop through points to draw cubic Bézier curves through the penultimate point, or through the last point if the line is closed.
			for (i = first; i < last - 1; i++){
				// BezierSegment instance using the current point, its second control point, the next point's first control point, and the next point
				bz = new BezierSegment(new Point(pts[i].x, pts[i].y), new Point(ctrlPts[i][1].x, ctrlPts[i][1].y), new Point(ctrlPts[i+1][0].x, ctrlPts[i+1][0].y), new Point(pts[i+1].x, pts[i+1].y));
				for (t = 0.01; t < 1.01; t += 0.01){
					v = bz.getValue(t);
					_commands.push(2);
					_data.push(v.x, v.y);
				}
			}
			
			if (last == pts.length - 1){
				_commands.push(3);
				_data.push(ctrlPts[i][1].x, ctrlPts[i][1].y, pts[i + 1].x, pts[i + 1].y);
			}
		}
		
	}
}
